﻿/* Copyright © 2012 Gargol, Gargol at gbot dot org
   Based on CoreDC Copyright 2007 © Johan Pettersson, Fredrik Johansson, Michael Starberg, Christian Epstein.
 
This file is part of ADCCore.

ADCCore is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

ADCCore is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with ADCCore.  If not, see <http://www.gnu.org/licenses/>. */
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace ADCCore.Implementation
{
    internal class DcConnection : IDisposable
    {
        static readonly int bufferSize = 65535;
        static readonly int ReconnectInterval = 60000;
        static readonly int readInterval = 200;
        readonly byte[] readBuffer = new byte[bufferSize];
        bool ssl = false;
        Encoding encoding = Encoding.UTF8;

        TcpClient client;
        NetworkStream stream;
        SslStream sslstream;

        private System.Windows.Forms.Timer readTimer;
        
        private System.Windows.Forms.Timer reconnectTimer;

        DateTime lastSent;

        StringBuilder readQueue;

        readonly DcClient dcClient;

        public DcConnection(DcClient client)
        {
            this.dcClient = client;
        }

        public void Open(string server, int port)
        {
            if (server.StartsWith("adcs://"))
            {
                server = server.Replace("adcs://","");
                ssl = true;
            }
            else
                server = server.Replace("adc://","");
            
            client = new TcpClient(server, port);
            if (ssl)
            {
                sslstream = new SslStream(client.GetStream(), true, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
                sslstream.AuthenticateAsClient(server, null, System.Security.Authentication.SslProtocols.Default, false);
            }
            else {
                stream = client.GetStream();
            }
            
            readTimer = new System.Windows.Forms.Timer();
            readTimer.Interval = readInterval;
            readTimer.Tick += readTimer_Tick;
            readTimer.Tag = "readTimer";
            readTimer.Start();



            lastSent = DateTime.Now;
        }
        internal static bool ValidateServerCertificate(object sender,X509Certificate certificate,X509Chain chain,SslPolicyErrors sslPolicyErrors)
        {
            return true;
            /*
            if (sslPolicyErrors == SslPolicyErrors.None)
                return true;

            Console.WriteLine("Certificate error: {0}", sslPolicyErrors);

            // Do not allow this client to communicate with unauthenticated servers.
            return false;*/
        }
        public static X509Certificate ValidateClientCertificate(
        object sender,
        string targetHost,
        X509CertificateCollection localCertificates,
        X509Certificate remoteCertificate,
        string[] acceptableIssuers)
        {
            byte[] newCert = Certificate.CreateSelfSignCertificatePfx("", DateTime.Now, DateTime.Now.AddYears(30));
            X509Certificate cert = new X509Certificate(newCert);
            return cert;
            //return null;
            /*
            Console.WriteLine(" > Client is selecting a local certificate.");
            if (acceptableIssuers != null &&
                acceptableIssuers.Length > 0 &&
                localCertificates != null &&
                localCertificates.Count > 0)
            {
                // Use the first certificate that is from an acceptable issuer.
                foreach (X509Certificate certificate in localCertificates)
                {
                    string issuer = certificate.Issuer;
                    if (Array.IndexOf(acceptableIssuers, issuer) != -1)
                        return certificate;
                }
            }
            if (localCertificates != null &&
                localCertificates.Count > 0)
                return localCertificates[0];

            return null;*/
        }

        public void Dispose()
        {
            Close();
        }

        public void Close()
        {
            readTimer.Dispose();
            if (stream != null) 
                stream.Close();
            if (sslstream != null)     
                sslstream.Close();
            
            client.Close();
        }

        public void Write(string message)
        {
            // TODO: Need to handle when the stream is closed (?)
            // "Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host."
            try
            {
                Byte[] data = encoding.GetBytes(message);

                if (ssl)
                {
                    lock (sslstream)
                    {
                        sslstream.Write(data, 0, data.Length);
                        sslstream.Flush();
                    }
                }
                else
                {
                    lock (stream)
                    {
                        stream.Write(data, 0, data.Length);
                        stream.Flush();
                    }
                }
                lastSent = DateTime.Now;
            }
            catch (System.IO.IOException ex)
            {
                // If we can't write to the stream, we've been disconnected
                dcClient.IsConnected = false;
                if (dcClient.ClientInfo.ReconnectOnDisconnect)
                {
                    StartReconnect();
                }
                dcClient.DoDisconnected(dcClient.ClientInfo.ReconnectOnDisconnect);
                Console.WriteLine(ex.Message);
                Dispose();
            }
            catch (Exception ex)
            {
                /*
                dcClient.IsConnected = false;
                if (dcClient.ClientInfo.ReconnectOnDisconnect)
                {
                    StartReconnect();
                }
                dcClient.DoDisconnected(dcClient.ClientInfo.ReconnectOnDisconnect);
                Dispose();*/
                Console.WriteLine(ex.Message);
                /*// Something else might be wrong perhaps?
                Exception res = dcClient.TryReport(dcClient, ex);
                if (res != null) throw;*/
            }
        }

        void readTimer_Tick(object sender, EventArgs e)
        {
            if (System.Threading.Thread.CurrentThread.Name == null)
            {
                System.Threading.Thread.CurrentThread.Name = "readTimer_Tick";
            }

            try
            {
                int bytesRead = 0;
                if (ssl)
                {
                    if (client.Available > 0)
                        bytesRead = sslstream.Read(readBuffer, 0, readBuffer.Length);
                }
                else
                {
                    if (stream.DataAvailable)
                        bytesRead = stream.Read(readBuffer, 0, readBuffer.Length);
                }
                if (bytesRead > 0)
                {
                    Read(encoding.GetString(readBuffer, 0, bytesRead));
                }
            }
            catch (System.IO.IOException)
            {
                // If we can't read from the stream, we've been disconnected
                dcClient.IsConnected = false;
                if (dcClient.ClientInfo.ReconnectOnDisconnect)
                {
                    StartReconnect();
                }
                dcClient.DoDisconnected(dcClient.ClientInfo.ReconnectOnDisconnect);
                Dispose();
            }
            catch (Exception ex)
            {
                /*dcClient.IsConnected = false;
                if (dcClient.ClientInfo.ReconnectOnDisconnect)
                {
                    StartReconnect();
                }
                dcClient.DoDisconnected(dcClient.ClientInfo.ReconnectOnDisconnect);
                
                Dispose();*/
                Console.WriteLine(ex.Message);
                // Something else might be wrong perhaps?
                //Exception res = dcClient.TryReport(dcClient, ex);
                //if (res != null) ;
            }
        }

        public void StartReconnect()
        {
            try
            {
                if (!reconnectTimer.Enabled)
                    return;
            }
            catch { }
            reconnectTimer = new System.Windows.Forms.Timer();
            reconnectTimer.Interval = ReconnectInterval;
            reconnectTimer.Tick += reconnectTimer_Tick;
            reconnectTimer.Tag = "reconnectTimer";
            reconnectTimer.Start();
        }

        
        void reconnectTimer_Tick(object sender, EventArgs e)
        {
                reconnectTimer.Stop();
                reconnectTimer.Dispose();
            if (!dcClient.IsConnected)
                dcClient.Connect();
        }
        void Read(string frame)
        {
            Console.WriteLine("Recive:> " + frame);
            string messageBundle = null;
            if (frame.EndsWith("\n"))
            {
                //message bundle is intact
                messageBundle = GetReadQueue() + frame;
            }
            else
            {
                int index = frame.LastIndexOf('\n');
                if (index == -1)
                {
                    //whole frame is part of message bundle
                    AddToReadQueue(frame);
                }
                else
                {
                    //part of frame is a message bundle
                    messageBundle = GetReadQueue() + frame.Substring(0, index + 1);
                    //while the rest must be queued
                    AddToReadQueue(frame.Substring(index + 1));
                }
            }

            if (messageBundle != null)
            {
                // split the bundle into seperate messages..
                string[] messages = messageBundle.Split("\n".ToCharArray());
                foreach (string message in messages)
                {
                    // but only handle messages that actually carries any information 
                    if (message.Length > 1)
                    {
                        // and trigger event for each message without the trailing pipe
                        OnMessage(this.dcClient, message.Substring(0, message.Length));
                    }
                }
            }
        }

        string GetReadQueue()
        {
            if (readQueue != null)
            {
                string s = readQueue.ToString();
                readQueue = null;
                return s;
            }
            return String.Empty;
        }

        void AddToReadQueue(string overflow)
        {
            if (readQueue == null)
            {
                readQueue = new StringBuilder();
            }
            readQueue.Append(overflow);
        }

        public event DcMessageHandler OnMessage;

    }
}
